Thanks to SHIVAM BANSAL's "Understanding Python Dataclasses — Part 1" on Medium for his great example.
The following jupyter notebook cell is a Fibonacci recursive function. It got called 166 times to get Fibonacci series 1~9 as shown below:
In [1]:
count = 0
def fibonacci(n):
global count
count += 1
print(f'Count:{count} calling fibonacci({n})')
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
print([fibonacci(n) for n in range(1, 9)])
A decorator is simply a function which takes a function as a parameter and returns a new decorated function which is a better revision over the given function. For example, in the following code, the cache
function is used as a decorator to remember the Fibonacci numbers that have already been computed. This way dramatically shrink the count from 166 down to only 9!
In [2]:
def cache(function):
cached_values = {} # Contains already computed values
def wrapping_function(*args):
if args not in cached_values:
# Call the function only if we haven't already done it for those parameters
cached_values[args] = function(*args)
return cached_values[args]
return wrapping_function
@cache
def fibonacci(n):
global count
count += 1
print(f'Count:{count} calling fibonacci({n})')
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
count = 0
print([fibonacci(n) for n in range(1, 9)])
Wow, amazing! I want to look into the decorator to understand how it works. I want to see the *args
in wrapping_function(*args)
, it looks strange to me as a new python programmer, and cached_values = {}
looks like a mathmetical set but I am not so sure because it can be a dictionary too, furthermore the function
is totally a mystery to me. It's time to invoke peforth:
In [3]:
import peforth
and add 3 lines to the above code as shown below with # <----
marked at the end of those lines. The 3rd line is a jupyter notebook magic that invokes peforth and use the command .source
to see the decorated Fibonacci function:
In [4]:
def cache(function):
cached_values = {} # Contains already computed values
def wrapping_function(*args):
if args not in cached_values:
# Call the function only if we haven't already done it for those parameters
cached_values[args] = function(*args)
# peforth breakpoint
if debug and args == (6,) : peforth.push(locals()).ok("bp>",cmd='to _locals_') # <----
return cached_values[args]
return wrapping_function
@cache
def fibonacci(n):
global count
count += 1
print(f'Count:{count} calling fibonacci({n})')
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
count = 0; debug = False # <----
print([fibonacci(n) for n in range(1, 9)])
%f fibonacci .source # <---- This is the decorated version of the fibonacci function
So now we know that Fibonacci function has been replaced by the wrapping_function()
defined in cache()
. We need to look into the definition to see more. So please change the 2nd # <----
line of the above example with the debug = False
to debug = True
as the below cell. That actually enables the breakpoint (the 1st # <----
line) because we want to see things inside the wrapping_function()
function. Run this jupyter notebook cell you reach to the peforth interprete state interface :
then we can access everything there. Type in args . cr
to see args; type in cached_values . cr
to see cached_values both in FORTH syntax. We can even see what the function
is by typing in function .source
where .source
is a peforth word
or command that prints the given function's source code.
In [5]:
%f __main__ :> debug --> # See 'debug' from peforth interpret state.
%f __main__ :: debug=12345 # Set a different value to the variable 'debug'l
%f __main__ :> debug --> # Check again.
%f __main__ :: debug=False # Turn off breakpoint
%f __main__ :> debug --> # Check again.
The way to stop debugging (turn off the breakpoint) and continue the origianl program :
bp> __main__ :: debug=False quit
So do it yourself now . . .
In [6]:
def cache(function):
cached_values = {} # Contains already computed values
def wrapping_function(*args):
if args not in cached_values: # means "the function takes an arbitrary number of arguments and they will be accessible through the list args.
# Call the function only if we haven't already done it for those parameters
cached_values[args] = function(*args)
# peforth breakpoint
if debug and args == (6,) : peforth.push(locals()).ok("bp>",cmd='to _locals_') # <----
return cached_values[args]
return wrapping_function
@cache
def fibonacci(n):
global count
count += 1
print(f'Count:{count} calling fibonacci({n})')
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
count = 0; debug = True # <----
print([fibonacci(n) for n in range(1, 9)])
%f fibonacci .source # <---- This is the decorated version of the fibonacci function
The first # <----
marked line shown above calls peforth:
if debug and args == (6,) : peforth.push(locals()).ok("bp>",cmd='to _locals_') # <----
What it says is : if condition is true then shell into peforth with all recent local variables passed over by an anonymous object that will appear on top of the data stack (TOS) of peforth, by the way the peforth command prompt is 'bp>' so we can tell which breakpoint it is among many. Also this line tells peforth to store the TOS to variable _locals_
.
With the above arrangements when we jump into peforth, or shell to it, the FORTH interpreter is a normal interactive interface but it knows all python global variables and also all local variables at the point where peforth is called. We can then see them and even modify them. Since peforth v1.23 the word 'unknown' is redefined by default, so you'll see reDef unknown
replied after import peforth
, the new definition will try to find an unknown token from python global variables and then _locals_
so we can access them from within peforth.
Execute 'quit' or 'exit' peforth command to leave from that breakpoint and continue the program. 'quit' command clears the peforth variable _locals_
while 'exit' leaves it as is. If we 'exit' peforth from within a python function we can still access the function's local variables after it has returned, that's strange but I like it for that doesn't hurt anybody, or does it?
I like you to know that peforth itself is very simple. Its kernel code projectk.py
has only 22k size include many comments. peforth is not at all a super hero but a very comfortable way to talk to computers. That's what I want to show you about what peforth can do. Please open issues to the GitHub project for your questions so I can explain more.
H.C. Chen @ FigTaiwan
hcchen5600@gmail.com